package com.googlecode.alvor.lexer.sql;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.googlecode.alvor.lexer.alphabet.IAbstractInputItem;
import com.googlecode.alvor.lexer.alphabet.IAbstractOutputItem;
import com.googlecode.alvor.lexer.alphabet.ISequence;
import com.googlecode.alvor.lexer.alphabet.PushInput;
import com.googlecode.alvor.lexer.alphabet.SequenceUtil;
import com.googlecode.alvor.lexer.alphabet.SimpleCharacter;
import com.googlecode.alvor.lexer.alphabet.Token;
import com.googlecode.alvor.lexer.alphabet.Yield;
import com.googlecode.alvor.lexer.automata.AutomataDeterminator;
import com.googlecode.alvor.lexer.automata.IAlphabetConverter;
import com.googlecode.alvor.lexer.automata.LexerData;
import com.googlecode.alvor.lexer.automata.State;
import com.googlecode.alvor.lexer.automata.Transition;

/**
 * Provides useful functions on lexer data generated by JFlex
 * 
 * @author abreslav
 *
 */
public class SQLLexer {

	/**
	 * Initial state of a transition diagram of a final-state transducer 
	 * (built from SQL lexer tables generated by JFlex)
	 */
	public final State SQL_TRANSDUCER;

	/**
	 * Converts Unicode symbols to the lexer character classes
	 */
	public final IAlphabetConverter SQL_ALPHABET_CONVERTER;

	// The code of ID token (needed to support keywords)
	private final int ID_CODE;
	
	// The set of keywords (all in upper-case) 
	private final Set<String> KEYWORDS = new HashSet<String>();
	
	// Maps token names back to codes in the lexer table
	private static final Map<String, Integer> TOKEN_NAME_TO_CODE = new HashMap<String, Integer>();

	private final LexerData lexerData;
	
	public SQLLexer(final LexerData lexerData) {
		this.lexerData = lexerData;
		this.SQL_TRANSDUCER = new AutomataConverter(lexerData).convert();
		this.SQL_ALPHABET_CONVERTER = new IAlphabetConverter() {
			@Override
			public int convert(int c) {
				if (c == -1) {
					return c;
				}
				return lexerData.CHAR_CLASSES[c];
			}
		};
		
		for (String keyword : this.lexerData.KEYWORDS) {
			KEYWORDS.add(keyword.toUpperCase());
		}
		
		TOKEN_NAME_TO_CODE.put("EOF", -1);
		int nonKeywordCount = this.lexerData.TOKENS.length;
		for (int i = 0; i < nonKeywordCount; i++) {
			TOKEN_NAME_TO_CODE.put(this.lexerData.TOKENS[i], i);
		}
		for (int i = 0; i < this.lexerData.KEYWORDS.length; i++) {
			TOKEN_NAME_TO_CODE.put(this.lexerData.KEYWORDS[i], nonKeywordCount + i);
		}
		ID_CODE = getCodeByName("ID");
		
	}
	
	public LexerData getLexerData() {
		return lexerData;
	}
	
	/**
	 * @return token code by name
	 */
	private int getCodeByName(String name) {
		Integer code = TOKEN_NAME_TO_CODE.get(name);
		if (code == null) {
			throw new IllegalArgumentException("Unknown token type: " + name);
		}
		return code;
	}
	
	/**
	 * Checks if the token is whitespace (or comment), i.e. invisible for the parser
	 */
	public boolean isWhitespace(int code) {
		return getTokenName(code).length() == 0;	
	}
	
	/**
	 * Checks if the token is an identifier 
	 */
	public boolean isIdentifier(int code) {
		return code == ID_CODE;	
	}
	
	/**
	 * Gets token name by code 
	 */
	public String getTokenName(int code) {
		if (code == -1) {
			return "EOF";
		}
		int nonKeywordCount = this.lexerData.TOKENS.length;
		if (code < nonKeywordCount) {
			return this.lexerData.TOKENS[code];
		}
		return this.lexerData.KEYWORDS[code - nonKeywordCount];
	}
	
	private static class AutomataConverter {
		private final LexerData lexerData;
		
		public AutomataConverter(LexerData lexerData) {
			this.lexerData = lexerData;
		}
		
		public State convert() {
			// Create states
			State[] states = new State[lexerData.STATE_COUNT + 1];
			for (int i = 0; i < lexerData.STATE_COUNT; i++) {
				states[i] = new State("S" + i, false);
			}
			State initialState = states[0];
			State EOFState = new State("EOF", true);
			states[lexerData.STATE_COUNT] = EOFState;
			
			// Create transitions (for recognizing a single token)
			List<Integer> acceptingStatesIndices = new ArrayList<Integer>();
			for (int fromIndex = 0; fromIndex < lexerData.STATE_COUNT; fromIndex++) {
				State from = states[fromIndex];
				for (char cc = 0; cc < lexerData.CHAR_CLASS_COUNT; cc++) {
					int toIndex = lexerData.TRANSITIONS[fromIndex][cc];
					if (toIndex != -1) {
						List<IAbstractOutputItem> actionList = new ArrayList<IAbstractOutputItem>();
						if (isImmediatelyGenerating(toIndex)) {
							int actionIndex = lexerData.ACTIONS[toIndex];
							if (actionIndex >= 0) {
								actionList.add(PushInput.INSTANCE);
								actionList.add(Yield.create(actionIndex));
							} else {
								throw new IllegalStateException();
							}
						} else {
							actionList.add(PushInput.INSTANCE);
						}
						Transition.create(from, states[toIndex], 
									SimpleCharacter.create(Integer.valueOf(cc)), 
									actionList);
					}
				}
				if (isAccepting(fromIndex)) {
					acceptingStatesIndices.add(fromIndex);
				}
			}

			Transition.create(
					initialState, EOFState, 
					IAbstractInputItem.EOF, 
					Collections.singletonList(Yield.create(-1))
			);
			
			// Make it circular
			// Imagine an \eps-transition into initialState from every accepting state
			// for immediately generating states these do not produce any output
			// for others they produce some output
			// eliminating these imaginary transitions gives us an automaton which recognizes 
			// a token stream
			Iterable<Transition> initialTransitions = initialState.getOutgoingTransitions();
			for (Integer stateIndex : acceptingStatesIndices) {
				State state = states[stateIndex];
				for (Transition transition : initialTransitions) {
					State to = transition.getTo();
					IAbstractInputItem inChar = transition.getInChar();
					List<IAbstractOutputItem> initialOutStr = transition.getOutput();
					List<IAbstractOutputItem> resultingOutStr = isImmediatelyGenerating(stateIndex) 
						? initialOutStr 
						: prepend(Yield.create(lexerData.ACTIONS[stateIndex]), initialOutStr);
					Transition.create(state, to, inChar, resultingOutStr);
				}
			}
			
			AutomataDeterminator.determinateWithPriorities(initialState);
			return initialState;
		}
		
		private boolean isAccepting(int state) {
			return (lexerData.ATTRIBUTES[state] & 0001) != 0;
		}

		private boolean isImmediatelyGenerating(int state) {
			return (lexerData.ATTRIBUTES[state] & 0010) != 0;
		}
	}
	
	private static <T> List<T> prepend(T item, List<? extends T> list) {
		ArrayList<T> result = new ArrayList<T>();
		result.add(item);
		result.addAll(list);
		return result;
	}
	
	public String tokenToString(Token token) {
		String tokenName = getTokenName(token.getType());
		ISequence<IAbstractInputItem> text = token.getText();
		String textToStr = SequenceUtil.toString(text);
		return tokenName + "[" + textToStr + "]";
	}

	/**
	 * @param id -- token text
	 * @return keyword token if id is a keyword or the code for ID token otherwise 
	 */
	public int getIdentifierTokenType(String id) {
		String upperCase = id.toUpperCase();
		if (KEYWORDS.contains(upperCase)) {
			return getCodeByName(upperCase);
		} 
		return ID_CODE; 
	}
}
